package org.spring.boot.func.container;

import org.spring.boot.func.annotation.CallbackClass;
import org.spring.boot.func.annotation.EnableLog;
import org.spring.boot.func.annotation.FuncLambda;
import org.spring.boot.func.annotation.FuncParameter;
import org.spring.boot.func.properties.FuncProperties;
import org.spring.boot.func.type.FuncLambdaType;
import org.spring.boot.func.utils.FuncString;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.type.MethodMetadata;
import org.springframework.util.Assert;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

/**
 * @author Yiur
 */
public class FuncMethod {

    public final static String REGEX_SOURCE = "[A-z]*[\\(][\\W\\w]*[\\)]";
    public final static String PARAMETER_TYPE = "[(][\\W\\w]*[)]";
    public static final Map<String, Boolean> THIS_CONSTRAINT = new HashMap<>();
    private String methodName;
    private Object resultType;
    private Class<?>[] parameterType;
    private List<String> parameterName;
    private String parameterSource;
    private String source;
    private boolean callback;
    private Class<?> callbackClass;
    private final FuncProperties.Logger logger = new FuncProperties.Logger();

    static {
        THIS_CONSTRAINT.put("methodName", true);
        THIS_CONSTRAINT.put("resultType", true);
        THIS_CONSTRAINT.put("parameterName", true);
        THIS_CONSTRAINT.put("parameterSource", true);
        THIS_CONSTRAINT.put("source", true);
        THIS_CONSTRAINT.put("callback", true);
        THIS_CONSTRAINT.put("callBackClass", true);
    }

    public FuncMethod(MethodMetadata methodMetadata, List<FuncMethod> funcMethods) {
        MergedAnnotations annotations = methodMetadata.getAnnotations();
        setMethodName(methodMetadata.getMethodName());

        try {
            Class<?> classObject = Class.forName(methodMetadata.getDeclaringClassName());
            for (Method method : classObject.getDeclaredMethods()) {
                Method currentMethod = getMethod(classObject, funcMethods, methodMetadata, method);

                if (currentMethod != null && currentMethod.getAnnotation(FuncLambda.class).bean().equals(annotations.get(FuncLambda.class).getString(FuncLambdaType.BEAN.value))) {
                    setParameterType(currentMethod.getParameterTypes());
                    List<String> currentParameter = new ArrayList<>();
                    for (Parameter parameter : currentMethod.getParameters()) {
                        Assert.notNull(parameter.getAnnotation(FuncParameter.class), FuncString.format("The parameter (?) of the current anonymous function (?) cannot be declared without @FuncParameter annotation", getMethodName(), parameter.getName()));
                        currentParameter.add(parameter.getAnnotation(FuncParameter.class).value());
                    }
                    setResultType(method.getReturnType());
                    setParameterName(currentParameter);
                    setParameterSource(FuncString.charLinkFormat(getParameterName(), ", "));
                    setSource(matcher(REGEX_SOURCE, methodMetadata.toString()));

                    MergedAnnotation<FuncLambda> funcLambda = annotations.get(FuncLambda.class);
                    MergedAnnotation<CallbackClass> bindCallback = annotations.get(CallbackClass.class);
                    boolean enableLog = annotations.get(EnableLog.class).isPresent();
                    setCallback(funcLambda.getBoolean(FuncLambdaType.CALLBACK.value));
                    setCallbackClass(bindCallback.isPresent() ? bindCallback.getClass(FuncLambdaType.CALLBACK_CLASS.value) : (Class<?>) CallbackClass.class.getDeclaredMethods()[0].getDefaultValue());
                    logger.setEnableLog(enableLog);
                    return;
                }
            }
        } catch (ClassNotFoundException ignored) {
        }
    }

    private Method getMethod(Class<?> classObject, List<FuncMethod> funcMethods, MethodMetadata methodMetadata, Method method) {
        Method currentMethod = null;
        try {
            if (!existFuncMethod(funcMethods, methodMetadata, method)) {
                try {
                    String parameterType = FuncString.removeParentheses(matcher(PARAMETER_TYPE, matcher(REGEX_SOURCE, methodMetadata.toString())));
                    if (!parameterType.equals(FuncString.STRING_EMPTY)) {
                        throw new NoSuchMethodException();
                    }
                    currentMethod = classObject.getMethod(methodMetadata.getMethodName());
                } catch (NoSuchMethodException e) {
                    currentMethod = classObject.getMethod(methodMetadata.getMethodName(), method.getParameterTypes());
                }
            }
        } catch (Exception ignored) {
        }
        return currentMethod;
    }

    private boolean existFuncMethod(List<FuncMethod> funcMethods, MethodMetadata methodMetadata, Method method) {
        boolean exist = false;
        String funcLinkMethod = matcher(REGEX_SOURCE, methodMetadata.toString());
        String linkMethod = matcher(REGEX_SOURCE, method.toString());
        for (FuncMethod funcMethod : funcMethods) {
            if (linkMethod.equals(funcMethod.getSource())) {
                exist = true;
            }
        }
        if (!funcLinkMethod.equals(linkMethod)) {
            exist = true;
        }
        return exist;
    }

    public static String matcher(String regex, String source) {
        return FuncString.matcher(regex, source);
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Object getResultType() {
        return resultType;
    }

    public void setResultType(Object resultType) {
        this.resultType = resultType;
    }

    public Class<?>[] getParameterType() {
        return parameterType;
    }

    public void setParameterType(Class<?>[] parameterType) {
        this.parameterType = parameterType;
    }

    public List<String> getParameterName() { return parameterName; }

    public void setParameterName(List<String> parameterName) {
        this.parameterName = parameterName;
    }

    public String getSource() {
        return source;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public boolean isCallback() {
        return callback;
    }

    public void setCallback(boolean callback) {
        this.callback = callback;
    }

    public Class<?> getCallbackClass() {
        return callbackClass;
    }

    public void setCallbackClass(Class<?> callbackClass) {
        this.callbackClass = callbackClass;
    }

    public FuncProperties.Logger getLogger() {
        return logger;
    }

    public String getParameterSource() {
        return parameterSource;
    }

    public void setParameterSource(String parameterSource) {
        this.parameterSource = parameterSource;
    }

}
